#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "math.h"
#include "gmp.h"

#define TAM_IP 16        /* 15 bytes + '\0'  */
#define TAM_HASH 33      /* 32 bytes + '\0'  */
#define STR_BASE 16      /* base hexadecimal */

/*TODO: checar se os parametros passados na entrada estão no PATTERN requerido:

 FILE-KEY é uma chave primária em forma
     de uma hash criptográfica MD5, portanto é necessário ter 32 dígitos
     hexadecimais, e.g. "d41d8cd98f00b204e9800998ecf8427e"

     IP é o identificador da máquina, representado com 4 bytes em
notação decimal,
     e.g., "192.0.2.235".

     RELIABILITY é a confiabilidade da máquina no intervalo de tempo
QUAL INTERVALO DE TEMPO?,
     representado como um número em ponto flutuante, e.g., "0.95".

     MIN_AUDITING_TIME é o menor intervalo de tempo de auditoria
suportado pela máquina, representado como um número em ponto flutuante
(EM SEGUNDOS?? DIAS??? HORAS???), e.g. "1.25".
*/

/*TODO: definir se RELIABILITY é para algum tempo default ou se
deve-se passar um intervalo de tempo inicial de auditoria. */

/*TODO: definir se MIN_AUDITING_TIME é segundos, dias ou horas. */

/* Estrutura que compoe as informacoes de uma maquina na rede */
typedef struct {
    char ip[TAM_IP];
    double intervalo_auditoria;
    double confiabilidade;
    char faixa_inicio[TAM_HASH];
    char faixa_fim[TAM_HASH];
} info_maquina;

/* Estrutura vetor de maquinas, que armazena todas as maquinas*/
typedef struct {
    int num_maquinas;
    info_maquina *vetor;
} vetor_maquinas;

/*---------------------------------------------------------------------------------------------------------------------*/

/* Dada uma maquina, retorna numa string o tamanho da faixa dessa maquina */
char *tamFaixa(info_maquina *maquina)
{
    mpz_t f_inicio, f_fim, f_tam;
    mpz_init(f_tam);

    mpz_init_set_str(f_inicio, maquina->faixa_inicio, STR_BASE); 
    mpz_init_set_str(f_fim, maquina->faixa_fim, STR_BASE); 

    /* Subtrai para obter o tamanho */
    mpz_sub(f_tam, f_fim, f_inicio);

    /* Obtem o valor absoluto */
    mpz_abs(f_tam, f_tam); 

    return mpz_get_str(NULL, STR_BASE, f_tam );
}

/* Funcao que insere uma maquina na rede. */
int insert(vetor_maquinas *maquinas, char *ip, double confiabilidade, double intervalo_auditoria) 
{
    int i, maior_i = 0;
    char *tam_faixa;

    mpz_t f_tam_faixa, f_maior_faixa, f_faixa_inicio;
    mpz_init(f_tam_faixa);
    mpz_init(f_maior_faixa);
    mpz_init(f_faixa_inicio);

    /* Cria a nova maquina */
    info_maquina nova_maquina;
    strcpy(nova_maquina.ip, ip);
    nova_maquina.ip[TAM_IP-1] = '\0';
    nova_maquina.confiabilidade = confiabilidade;
    nova_maquina.intervalo_auditoria = intervalo_auditoria;

    /*Se já existe máquina no arquivo*/
    if (maquinas->num_maquinas > 0) {

        /* Busca maior faixa */
        for (i = 0 ;i < maquinas->num_maquinas; i++)
        {
            tam_faixa = tamFaixa( &(maquinas->vetor[i]) );
            mpz_set_str(f_tam_faixa, tam_faixa, STR_BASE);

            if (mpz_cmp(f_tam_faixa, f_maior_faixa) > 0)
            {
                mpz_set(f_maior_faixa, f_tam_faixa);
                maior_i = i;
            }
        }
   
        /*A nova maquina ocupara a primeira metade da faixa da maquina de maior faixa */
        strcpy(nova_maquina.faixa_inicio, maquinas->vetor[maior_i].faixa_inicio);

        /*reutilizando variavel f_maior_faixa como uma variavel auxiliar*/
        mpz_set_str(f_faixa_inicio, nova_maquina.faixa_inicio, STR_BASE);
        mpz_cdiv_q_ui(f_maior_faixa, f_maior_faixa, 2);
        mpz_add(f_maior_faixa, f_maior_faixa, f_faixa_inicio);
        strcpy(nova_maquina.faixa_fim, mpz_get_str(NULL, STR_BASE, f_maior_faixa)); /*TODO: soma com overflow*/

        mpz_add_ui(f_maior_faixa, f_maior_faixa, 1);
        strcpy(maquinas->vetor[maior_i].faixa_inicio, mpz_get_str(NULL, STR_BASE, f_maior_faixa));
    }
    /*Senão, então essa é a primeira máquina*/
    else {
        for (i = 0; i < TAM_HASH; i++) {
            nova_maquina.faixa_inicio[i] = '0';
            nova_maquina.faixa_fim[i] = 'f';
        }
        nova_maquina.faixa_inicio[TAM_HASH-1] = '\0';
        nova_maquina.faixa_fim[TAM_HASH-1] = '\0';
    }

    /* Faz um "shift" na maquina de maior faixa e seus sucessores para abrigar a nova maquina */
    maquinas->num_maquinas++;
    maquinas->vetor = realloc(maquinas->vetor, maquinas->num_maquinas * sizeof(info_maquina));

    if (!(maquinas->vetor)) {
        printf("Não foi possível alocar memória");
        return 0;
    }

    for( i = maquinas->num_maquinas-1; i > maior_i; i--)
    {
        strcpy(maquinas->vetor[i].ip, maquinas->vetor[i-1].ip);
        maquinas->vetor[i].intervalo_auditoria = maquinas->vetor[i-1].intervalo_auditoria;
        maquinas->vetor[i].confiabilidade = maquinas->vetor[i-1].confiabilidade;
        strcpy(maquinas->vetor[i].faixa_inicio, maquinas->vetor[i-1].faixa_inicio);
        strcpy(maquinas->vetor[i].faixa_fim, maquinas->vetor[i-1].faixa_fim);
    }
   
    /* Inclui a nova maquina */
    strcpy(maquinas->vetor[maior_i].ip, nova_maquina.ip);
    maquinas->vetor[maior_i].intervalo_auditoria = nova_maquina.intervalo_auditoria;
    maquinas->vetor[maior_i].confiabilidade = nova_maquina.confiabilidade;
    strcpy(maquinas->vetor[maior_i].faixa_inicio, nova_maquina.faixa_inicio);
    strcpy(maquinas->vetor[maior_i].faixa_fim, nova_maquina.faixa_fim);

    printf("Nova maquina inserida com sucesso\n");
    return 1;
}

/* Funcao que remove uma maquina da rede. */
int delete(vetor_maquinas *maquinas, char *ip)
{
    int i, i_del = -1;

    mpz_t f_tam_esquerdo, f_tam_direito;
    mpz_init(f_tam_esquerdo);
    mpz_init(f_tam_direito);

    /* Encontra a maquina a ser removida */
    for (i = 0 ; i < maquinas->num_maquinas ; i++ )
        if ( !strcmp(ip, maquinas->vetor[i].ip) )
            i_del = i;
    
    /* Caso nao tenha encontrado */    
    if (i_del == -1) 
    {
        printf("Nao existe maquina com o IP fornecido." );
        return 0;
    }

    /*Se a máquina não tiver vizinho*/
    if (maquinas->num_maquinas == 1)
    {
        maquinas->num_maquinas--;
        free(maquinas->vetor);
        maquinas->vetor = NULL;
    }
    /*Senão, a máquina possui vizinho(s)*/
    else
    {
        /*Se a máquina encontrada possui vizinho da esquerda*/
        if (i_del > 0)
        {
            /*Se possui, também vizinho da direita*/
            if (i_del < (maquinas->num_maquinas - 1))
            {
                /* Dentre os dois vizinhos, escolhe o que tem menor faixa para ocupar o lugar do que foi removido */  
                mpz_set_str(f_tam_esquerdo, tamFaixa(&(maquinas->vetor[i_del-1])), STR_BASE);
                mpz_set_str(f_tam_direito, tamFaixa(&(maquinas->vetor[i_del+1])), STR_BASE);

                if (mpz_cmp(f_tam_esquerdo, f_tam_direito) < 0)
                    strcpy(maquinas->vetor[i_del-1].faixa_fim, maquinas->vetor[i_del].faixa_fim);
                else
                    strcpy(maquinas->vetor[i_del+1].faixa_inicio, maquinas->vetor[i_del].faixa_inicio);
            }
            else
            {
                strcpy(maquinas->vetor[i_del-1].faixa_fim, maquinas->vetor[i_del].faixa_fim);
            }
        }
        /*Então possui apenas vizinho da direita*/
        else {
            strcpy(maquinas->vetor[i_del+1].faixa_inicio, maquinas->vetor[i_del].faixa_inicio);
        }

        /* Faz um "shift esquerda" nos sucessores da maquina removida*/
        maquinas->num_maquinas--;
        for( i = i_del; i < maquinas->num_maquinas; i++ )
        {
            strcpy(maquinas->vetor[i].ip, maquinas->vetor[i+1].ip);
            maquinas->vetor[i].intervalo_auditoria = maquinas->vetor[i+1].intervalo_auditoria;
            maquinas->vetor[i].confiabilidade = maquinas->vetor[i+1].confiabilidade;
            strcpy(maquinas->vetor[i].faixa_inicio, maquinas->vetor[i+1].faixa_inicio);
            strcpy(maquinas->vetor[i].faixa_fim, maquinas->vetor[i+1].faixa_fim);
        }
    }

    return 1;
}

int busca_binaria(mpz_t hash, int inicio, int fim, vetor_maquinas *maquinas) {    
    int meio;

    mpz_t f_fim, f_inicio;

    if (inicio > fim) return -1;

    meio = inicio + floor( (fim - inicio + 1)/2 );
    mpz_init_set_str(f_inicio, maquinas->vetor[meio].faixa_inicio, STR_BASE);
    mpz_init_set_str(f_fim, maquinas->vetor[meio].faixa_fim, STR_BASE);

    if ( (mpz_cmp(hash,f_fim) <= 0) && (mpz_cmp(hash,f_inicio) >= 0) )
        return meio;

    if (mpz_cmp(hash,f_inicio) <= 0)
        return busca_binaria(hash, inicio, meio - 1, maquinas);
    else
        return busca_binaria(hash, meio + 1, fim, maquinas);
}

/* Funcao que busca uma maquina ou mais maquinas a partir de uma hash ou de uma lista de hash*/
int search_list(vetor_maquinas *maquinas, char* arq_name)
{
    FILE *arq = fopen(arq_name, "r");
    char hash[TAM_HASH];
    int res = 0;

    if (!arq) {
        printf("Arquivo não encontrado\n");
        return 1;
    }

    while (fgets(hash, TAM_HASH, arq)) {
        res += search(maquinas, hash);
        fgets(hash, TAM_HASH, arq); /*consome '\n'*/
    }

    return res;
}

/* Funcao que busca uma maquina na rede a partir de uma hash. */
int search(vetor_maquinas *maquinas, char *hash) 
{
    mpz_t num_hash;    

    mpz_init_set_str(num_hash, hash, STR_BASE);        

    int inicio = 0;
    int fim = maquinas->num_maquinas - 1;
    int maq_encontrada;

    maq_encontrada = busca_binaria(num_hash, inicio, fim, maquinas);

    if ( maq_encontrada > -1 ) {
        printf("%s => %s\n", hash, maquinas->vetor[maq_encontrada].ip);
        return 0;
    }
    else {
        printf("Erro! Hash nao encontrada!\n");
        return 1;
    }
}

/* Le arquivo e carrega a matriz de informacoes das maquinas */
vetor_maquinas *le_arquivo()
{
    FILE *arq = fopen(".arq_maq.info","r+b");      

    vetor_maquinas *maquinas = malloc(sizeof(vetor_maquinas));

    if (!arq) {
        maquinas->num_maquinas = 0;
        maquinas->vetor = NULL;
    }
    else {
        fread( &(maquinas->num_maquinas), sizeof(int), 1, arq);
        maquinas->vetor = malloc(sizeof(info_maquina) * maquinas->num_maquinas);
        fread(maquinas->vetor, sizeof(info_maquina), maquinas->num_maquinas, arq);
        fclose(arq);
    }

    return maquinas;
}    

/* Escreve arquivo de acordo com as informacoes das maquinas */
void escreve_arquivo(vetor_maquinas *maquinas)
{
    FILE *arq = fopen(".arq_maq.info","w+b");

    fwrite(&(maquinas->num_maquinas), sizeof(int), 1, arq);
    fwrite(maquinas->vetor, sizeof(info_maquina), maquinas->num_maquinas, arq);
    
    fclose(arq);
}

char *testa_ip(char *ip_arg) {
    int i = 0, j, count = 0, num_pontos = 0;
    int tam = strlen(ip_arg);
    char aux[2], matriz[4][3], *ip = malloc(sizeof(char) * TAM_IP);

    while ((i < tam) && (count < 4) && (num_pontos < 4)) {
        if (ip_arg[i] != '.')
            matriz[num_pontos][count++] = ip_arg[i];
        else {
            if (count < 3) {
                for (j = 0; j < count; j++)
                    aux[j] = matriz[num_pontos][j];
                j = 0;
                while (j < (3-count))
                    matriz[num_pontos][j++] = '0';
                for ( ; j < 3; j++)
                    matriz[num_pontos][j] = aux[j-(3-count)];
            }
            num_pontos++;
            count = 0;
        }
        i++;
    }
    if (count < 3) {
        for (j = 0; j < count; j++)
            aux[j] = matriz[num_pontos][j];
        j = 0;
        while (j < (3-count))
            matriz[num_pontos][j++] = '0';
        for ( ; j < 3; j++)
            matriz[num_pontos][j] = aux[j-(3-count)];
    }

    if ( (num_pontos != 3) || (count > 3)) 
        return NULL;

    num_pontos = 0;
    for (i = 0; i < 4; i++) {
        for(count = 0; count < 3; count++)
            ip[(i*3)+count+num_pontos] = matriz[i][count];
        ip[(i*3)+count+num_pontos] = '.';
        num_pontos++;
    }
    ip[TAM_IP-1] = '\0';
    return ip;
}

/*********************************/
/*           FUNCAO  MAIN        */
/*********************************/
int main (int argc, char *argv[]) 
{
    char *ip;
    int i, opcao = 0;
    vetor_maquinas *maquinas;

    /*testando argumentos*/
    if ( argc < 2 ) {
        printf("Usage: %s {--insert|--delete|--search}\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    if ( !strcmp("--insert", argv[1]) ) {
        if ( argc < 5 ) {
            printf("Usage: %s --insert IP RELIABILITY MIN_AUDITING_TIME\n", argv[0]);
            exit(EXIT_FAILURE);
        }
        if ((ip = testa_ip(argv[2])) == NULL) {
            printf("IP inválido\n");
            exit(EXIT_FAILURE);
        };
        opcao = 1;
    }
    else if ( !strcmp("--delete", argv[1]) ) {
        if ( argc < 3 ) {
            printf("Usage: %s --delete IP\n", argv[0]);
            exit(EXIT_FAILURE);
        }
        if ((ip = testa_ip(argv[2])) == NULL) {
            printf("IP inválido\n");
            exit(EXIT_FAILURE);
        };
        opcao = 2;
    }
    else if ( !strcmp("--search", argv[1]) ) {
        if ( argc < 3 ) {
            printf("Usage: %s --search FILE-KEY\n", argv[0]);
            exit(EXIT_FAILURE);
        }
        opcao = 3;
    }
    else if ( !strcmp("--search-list", argv[1]) ) {
        if ( argc < 3 ) {
            printf("Usage: %s --search-list FILE_NAME\n", argv[0]);
            exit(EXIT_FAILURE);
        }
        opcao = 4;
    }
    else {
        printf("Usage: %s {--insert|--delete|--search}\n", argv[0]);
        exit(EXIT_FAILURE);
    }

   /* Le o arquivo */
   maquinas = le_arquivo();

   /* Chama as funcoes */
   switch(opcao) {
      case 1:  if ( !insert(maquinas, ip, atof(argv[3]), atof(argv[4])) )
                   exit(EXIT_FAILURE);
               break;
      case 2:  if ( !delete(maquinas, ip) )
                   exit(EXIT_FAILURE);
               break;
      case 3:  if ( !search(maquinas, argv[2]) )
                   exit(EXIT_FAILURE);
               break;
      case 4:  if ( !search_list(maquinas, argv[2]) )
                   exit(EXIT_FAILURE);
               break;
   }
  
   /* Escreve o arquivo */
   escreve_arquivo(maquinas);
 
   exit(EXIT_SUCCESS);
}
